package com.hongji.lms5xxcommunicator.utils;

import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * <p>通用的 SICK CoLa-A TCP 命令客户端。</p>
 *
 * <p><b>职责：</b></p>
 * <ul>
 *   <li>建立与 SICK 设备的 TCP 连接并维持会话（可复用）。</li>
 *   <li>封包发送：在 ASCII 命令体前后自动添加 STX(0x02)/ETX(0x03)。</li>
 *   <li>按字节读取回应：忽略开头 STX，直到遇到 ETX 截止；返回去掉 STX/ETX 的纯 ASCII 文本。</li>
 *   <li>提供超时控制以避免阻塞。</li>
 *   <li>一次性交互便捷方法：{@link #oneShot(String, int, int, String)}。</li>
 * </ul>
 *
 * <p><b>线程安全：</b></p>
 * <ul>
 *   <li>关键方法加 {@code synchronized}，保证单实例串行访问；不适合高并发。</li>
 *   <li>如需高并发，请自行管理连接池或多实例。</li>
 * </ul>
 *
 * @since 1.0
 */
@Slf4j
public class ColaACommandClient implements Closeable {

    /** STX/ETX 控制字符（ASCII） */
    private static final int STX = 0x02;
    private static final int ETX = 0x03;

    /** 目标主机 */
    private final String host;
    /** 目标端口（1~65535） */
    private final int port;
    /** Socket 读写超时（毫秒） */
    private final int timeoutMs;

    /** 底层 Socket 连接 */
    private Socket socket;
    /** 输入流（ASCII，带缓冲） */
    private BufferedReader in;
    /** 输出流（ASCII，带缓冲） */
    private PrintWriter out;

    /**
     * <p>构造 CoLa-A 客户端。</p>
     *
     * @param host 目标主机名/IP（非空）
     * @param port 目标端口（1~65535）
     * @param timeoutMs 读超时（毫秒，&gt;0）
     * @throws IllegalArgumentException 参数非法时抛出
     */
    public ColaACommandClient(String host, int port, int timeoutMs) {
        if (host == null || host.trim().isEmpty()) {
            throw new IllegalArgumentException("host 不能为空");
        }
        if (port <= 0 || port > 65535) {
            throw new IllegalArgumentException("port 必须在 1~65535 之间");
        }
        if (timeoutMs <= 0) {
            throw new IllegalArgumentException("timeoutMs 必须 > 0");
        }
        this.host = host.trim();
        this.port = port;
        this.timeoutMs = timeoutMs;
    }

    /**
     * <p>建立连接（幂等）。</p>
     * <ol>
     *   <li>若已连接且未关闭，直接返回；</li>
     *   <li>否则新建 {@link Socket}，设置 {@link Socket#setSoTimeout(int)} 并初始化流。</li>
     * </ol>
     *
     * @throws IOException 连接失败时抛出
     */
    public synchronized void connect() throws IOException {
        if (socket != null && socket.isConnected() && !socket.isClosed()) {
            return; // 已连接，无需重复
        }
        this.socket = new Socket(host, port);
        this.socket.setSoTimeout(timeoutMs);
        this.out = new PrintWriter(
                new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII)),
                true
        );
        this.in  = new BufferedReader(
                new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII)
        );
        log.info("CoLa-A 已连接：{}:{}", host, port);
    }

    /**
     * <p>是否已连接可用。</p>
     *
     * @return {@code true} 表示 socket 已建立且未关闭
     */
    public synchronized boolean isConnected() {
        return socket != null && socket.isConnected() && !socket.isClosed();
    }

    /**
     * <p>发送登录命令（可选）。</p>
     *
     * @param loginCommand 登录命令字符串（ASCII，不含 STX/ETX）；为空则跳过
     * @return 登录响应（去掉 STX/ETX），若未发送返回 {@code null}
     * @throws IOException I/O 异常或超时
     */
    public synchronized String login(String loginCommand) throws IOException {
        if (loginCommand == null || loginCommand.isEmpty()) return null;
        return sendCommand(loginCommand);
    }

    /**
     * <p>发送一条 CoLa-A ASCII 命令。</p>
     *
     * <p>行为：</p>
     * <ul>
     *   <li>自动封包（STX + 命令体 + ETX）；</li>
     *   <li>阻塞读取直到遇到 ETX 截止；</li>
     *   <li>忽略响应开头的 STX，返回去掉 STX/ETX 的 ASCII 文本。</li>
     * </ul>
     *
     * @param asciiBody 纯 ASCII 命令体（不含 STX/ETX），非空
     * @return 去掉 STX/ETX 的 ASCII 响应
     * @throws IOException 连接断开、超时或 I/O 异常
     * @throws IllegalArgumentException 当命令体为空时
     */
    public synchronized String sendCommand(String asciiBody) throws IOException {
        if (asciiBody == null || asciiBody.isEmpty()) {
            throw new IllegalArgumentException("asciiBody 不能为空");
        }
        if (!isConnected()) {
            throw new EOFException("CoLa-A 未连接");
        }

        // 构造报文：STX + body + ETX
        String pkt = "\u0002" + asciiBody + "\u0003";
        out.print(pkt);
        out.flush();
        log.debug("CoLa-A => {}", asciiBody);

        StringBuilder sb = new StringBuilder(256);
        int ch;
        long deadline = System.currentTimeMillis() + timeoutMs;
        boolean started = false; // 是否已经越过（或忽略）响应开头的 STX

        // 循环读直到 ETX
        while (true) {
            if (System.currentTimeMillis() > deadline) {
                throw new IOException("等待 CoLa-A 响应超时：" + asciiBody);
            }
            ch = in.read();
            if (ch == -1) {
                throw new EOFException("CoLa-A 连接被对端关闭");
            }
            if (!started) {
                // 忽略响应前导 STX（如果有）
                if (ch == STX) {
                    started = true;
                    continue;
                } else {
                    // 某些设备可能不回 STX，直接开始数据
                    started = true;
                }
            }
            if (ch == ETX) { // 读到 ETX 结束
                break;
            }
            sb.append((char) ch);
        }
        String resp = sb.toString();
        log.debug("CoLa-A <= {}", resp);
        return resp;
    }

    /**
     * <p>便捷方法：一次性连接 → 发送 → 关闭。</p>
     *
     * <p>适合临时请求场景，不适合高频调用（每次都建立/关闭连接）。</p>
     *
     * @param host 目标主机
     * @param port 目标端口
     * @param timeoutMs 超时（毫秒）
     * @param asciiBody ASCII 命令体（不含 STX/ETX）
     * @return 响应（去掉 STX/ETX）
     * @throws IOException I/O 异常
     */
    public static String oneShot(String host, int port, int timeoutMs, String asciiBody) throws IOException {
        try (ColaACommandClient c = new ColaACommandClient(host, port, timeoutMs)) {
            c.connect();
            return c.sendCommand(asciiBody);
        }
    }

    /**
     * <p>关闭连接，释放资源（幂等）。</p>
     * <ul>
     *   <li>安全关闭输出/输入流与 socket；</li>
     *   <li>多次调用不报错；</li>
     *   <li>发生 IO 异常时向上抛出最后一个异常。</li>
     * </ul>
     */
    @Override
    public synchronized void close() throws IOException {
        IOException thrown = null;
        try { if (out != null) out.flush(); } catch (Exception ignore) {}
        try { if (out != null) out.close(); } catch (Exception ignore) {}
        try { if (in  != null) in.close();  } catch (IOException e) { thrown = e; }
        try { if (socket != null) socket.close(); } catch (IOException e) { thrown = e; }
        finally {
            socket = null; in = null; out = null;
        }
        if (thrown != null) throw thrown;
        log.info("CoLa-A 连接已关闭：{}:{}", host, port);
    }
}
